{
 "cells": [
  {
   "cell_type": "code",
   "execution_count": 1,
   "id": "9fbf6701",
   "metadata": {},
   "outputs": [],
   "source": [
    "import warnings\n",
    "warnings.filterwarnings('ignore')"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 2,
   "id": "cd031163",
   "metadata": {},
   "outputs": [],
   "source": [
    "import os\n",
    "import gc\n",
    "import numpy as np\n",
    "import pandas as pd\n",
    "import re\n",
    "import time\n",
    "from scipy import stats\n",
    "import matplotlib.pyplot as plt\n",
    "import category_encoders as ce\n",
    "import networkx as nx\n",
    "import pickle\n",
    "import lightgbm as lgb\n",
    "import catboost as cat\n",
    "import xgboost as xgb\n",
    "from datetime import timedelta\n",
    "from gensim.models import Word2Vec\n",
    "from io import StringIO\n",
    "from tqdm import tqdm\n",
    "from lightgbm import LGBMClassifier\n",
    "from lightgbm import log_evaluation, early_stopping\n",
    "from sklearn.metrics import roc_curve\n",
    "from scipy.stats import chi2_contingency, pearsonr\n",
    "from sklearn.preprocessing import StandardScaler, OneHotEncoder, LabelEncoder\n",
    "from sklearn.feature_extraction.text import TfidfVectorizer, CountVectorizer\n",
    "from sklearn.feature_extraction import FeatureHasher\n",
    "from sklearn.model_selection import StratifiedKFold, KFold, train_test_split, GridSearchCV\n",
    "from category_encoders import TargetEncoder\n",
    "from sklearn.decomposition import TruncatedSVD\n",
    "from autogluon.tabular import TabularDataset, TabularPredictor, FeatureMetadata\n",
    "from autogluon.features.generators import AsTypeFeatureGenerator, BulkFeatureGenerator, DropUniqueFeatureGenerator, FillNaFeatureGenerator, PipelineFeatureGenerator\n",
    "from autogluon.features.generators import CategoryFeatureGenerator, IdentityFeatureGenerator, AutoMLPipelineFeatureGenerator\n",
    "from autogluon.common.features.types import R_INT, R_FLOAT\n",
    "from autogluon.core.metrics import make_scorer"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "bb74bb1e",
   "metadata": {},
   "source": [
    "# 数据导入"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "63ea38d9",
   "metadata": {},
   "source": [
    "## 数据导入通用函数"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 3,
   "id": "8298a200",
   "metadata": {},
   "outputs": [],
   "source": [
    "def load_data_from_directory(directory):\n",
    "    \"\"\"\n",
    "    遍历目录加载所有CSV文件，将其作为独立的DataFrame变量\n",
    "\n",
    "    参数:\n",
    "    - directory: 输入的数据路径\n",
    "    \n",
    "    返回:\n",
    "    - 含有数据集名称的列表\n",
    "    \"\"\"\n",
    "    dataset_names = []\n",
    "    for filename in os.listdir(directory):\n",
    "        if filename.endswith(\".csv\"):\n",
    "            dataset_name = os.path.splitext(filename)[0] + '_data' # 获取文件名作为变量名\n",
    "            file_path = os.path.join(directory, filename)  # 完整的文件路径\n",
    "            globals()[dataset_name] = pd.read_csv(file_path)  # 将文件加载为DataFrame并赋值给全局变量\n",
    "            dataset_names.append(dataset_name)\n",
    "            print(f\"数据集 {dataset_name} 已加载为 DataFrame\")\n",
    "\n",
    "    return dataset_names"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "1b99c9f1",
   "metadata": {},
   "source": [
    "# 导入数据"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "24ed8e60",
   "metadata": {},
   "source": [
    "### 训练集导入"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 4,
   "id": "5d006341",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "数据集 TRAIN_AGET_PAY_data 已加载为 DataFrame\n",
      "数据集 TRAIN_ASSET_data 已加载为 DataFrame\n",
      "数据集 TRAIN_CCD_TR_DTL_data 已加载为 DataFrame\n",
      "数据集 TRAIN_MB_PAGEVIEW_DTL_data 已加载为 DataFrame\n",
      "数据集 TRAIN_MB_QRYTRNFLW_data 已加载为 DataFrame\n",
      "数据集 TRAIN_MB_TRNFLW_data 已加载为 DataFrame\n",
      "数据集 TRAIN_NATURE_data 已加载为 DataFrame\n",
      "数据集 TRAIN_PROD_HOLD_data 已加载为 DataFrame\n",
      "数据集 TRAIN_TARGET_INFO_data 已加载为 DataFrame\n",
      "数据集 TRAIN_TR_APS_DTL_data 已加载为 DataFrame\n",
      "数据集 TRAIN_TR_IBTF_data 已加载为 DataFrame\n",
      "数据集 TRAIN_TR_TPAY_data 已加载为 DataFrame\n"
     ]
    }
   ],
   "source": [
    "train_load_dt = '../Train_Data'\n",
    "train_data_name = load_data_from_directory(train_load_dt)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "d532a380",
   "metadata": {},
   "source": [
    "### A测试集导入"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 5,
   "id": "099d9841",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "数据集 A_AGET_PAY_data 已加载为 DataFrame\n",
      "数据集 A_ASSET_data 已加载为 DataFrame\n",
      "数据集 A_CCD_TR_DTL_data 已加载为 DataFrame\n",
      "数据集 A_MB_PAGEVIEW_DTL_data 已加载为 DataFrame\n",
      "数据集 A_MB_QRYTRNFLW_data 已加载为 DataFrame\n",
      "数据集 A_MB_TRNFLW_data 已加载为 DataFrame\n",
      "数据集 A_NATURE_data 已加载为 DataFrame\n",
      "数据集 A_PROD_HOLD_data 已加载为 DataFrame\n",
      "数据集 A_TARGET_data 已加载为 DataFrame\n",
      "数据集 A_TR_APS_DTL_data 已加载为 DataFrame\n",
      "数据集 A_TR_IBTF_data 已加载为 DataFrame\n",
      "数据集 A_TR_TPAY_data 已加载为 DataFrame\n"
     ]
    }
   ],
   "source": [
    "A_load_dt = '../DATA'\n",
    "A_data_name = load_data_from_directory(A_load_dt)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "b45d0db1",
   "metadata": {},
   "source": [
    "# 特征工程"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 26,
   "id": "c79ccd81",
   "metadata": {},
   "outputs": [],
   "source": [
    "AGET_PAY_data = A_AGET_PAY_data.copy()\n",
    "CCD_TR_DTL_data = A_CCD_TR_DTL_data.copy()"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "3719b46f",
   "metadata": {},
   "source": [
    "## 通用特征处理函数"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 27,
   "id": "fbc4e552",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "通用函数定义完成\n"
     ]
    }
   ],
   "source": [
    "# ==================== 日期转换函数 ====================\n",
    "\n",
    "def get_days_to_now(df, date_col):\n",
    "    \"\"\"\n",
    "    将日期列转换为距今天数特征\n",
    "    \n",
    "    参数:\n",
    "    - df: 数据框\n",
    "    - date_col: 日期列名\n",
    "    \n",
    "    返回:\n",
    "    - 添加了时间特征的数据框\n",
    "    \"\"\"\n",
    "    # 日期转换\n",
    "    df[\"date\"] = pd.to_datetime(df[date_col], format=\"%Y%m%d\")\n",
    "    \n",
    "    # 计算距最大日期的天数\n",
    "    max_date = df[\"date\"].max()\n",
    "    df_days_to_now = (max_date - df[\"date\"]).dt.days\n",
    "    \n",
    "    # 添加时间维度特征\n",
    "    df[\"date_months_to_now\"] = df_days_to_now // 31  # 距今月数(0, 1, 2对应最近3个月)\n",
    "    df[\"date_weeks_to_now\"] = df_days_to_now // 7    # 距今周数\n",
    "    df[\"date_days_to_now\"] = df_days_to_now          # 距今天数\n",
    "    \n",
    "    print(f\"日期转换完成:\")\n",
    "    print(f\"交易日期范围: {df['date'].min().date()} ~ {df['date'].max().date()}\")\n",
    "    print(f\"距今天数范围: {df['date_days_to_now'].min()} ~ {df['date_days_to_now'].max()}\")\n",
    "    print(f\"月份分布:\\n{df['date_months_to_now'].value_counts().sort_index()}\")\n",
    "    \n",
    "    return df\n",
    "\n",
    "# ==================== 通用聚合函数 ====================\n",
    "\n",
    "def get_dense_features(df, col, stat):\n",
    "    \"\"\"按客户ID聚合数值特征\"\"\"\n",
    "    if stat == \"kurt\":\n",
    "        f_stat = lambda x: x.kurt()\n",
    "    elif stat == \"quantile_1_4\":\n",
    "        f_stat = lambda x: x.quantile(0.25)\n",
    "    elif stat == \"quantile_1_2\":\n",
    "        f_stat = lambda x: x.quantile(0.5)\n",
    "    elif stat == \"quantile_3_4\":\n",
    "        f_stat = lambda x: x.quantile(0.75)\n",
    "    else:\n",
    "        f_stat = stat\n",
    "    \n",
    "    group_df = df.groupby(['CUST_NO'])[col].agg(f_stat).reset_index()\n",
    "    group_df.columns = ['CUST_NO', 'CUST_NO_'+'{}_'.format(col)+stat]\n",
    "    return group_df\n",
    "\n",
    "def get_all_dense_features(df_fea, df_to_groupby, stats):\n",
    "    \"\"\"批量生成数值型特征的统计量\"\"\"\n",
    "    dense_col = [col for col in df_to_groupby.columns if col != \"CUST_NO\"]\n",
    "    for col in tqdm(dense_col, desc=\"生成数值特征\"):\n",
    "        for stat in stats:\n",
    "            df_fea = df_fea.merge(get_dense_features(df_to_groupby, col, stat), on='CUST_NO', how='left')\n",
    "    return df_fea\n",
    "\n",
    "def get_id_category_features(df_fea, df_to_groupby, fea1, fea2, stat):\n",
    "    \"\"\"\n",
    "    按客户ID和类别特征聚合\n",
    "    fea1: 类别特征名(如交易代码、渠道)\n",
    "    fea2: 要聚合的数值特征名\n",
    "    stat: 统计函数\n",
    "    \"\"\"\n",
    "    tmp = df_to_groupby.groupby(['CUST_NO', fea1])[fea2].agg(\n",
    "        stat if stat != \"kurt\" else lambda x: x.kurt()\n",
    "    ).to_frame(\n",
    "        '_'.join(['CUST_NO', fea1, fea2, stat])\n",
    "    ).reset_index()\n",
    "    \n",
    "    # 透视表: 将类别特征展开为多列\n",
    "    df_tmp = pd.pivot(data=tmp, index='CUST_NO', columns=fea1, values='_'.join(['CUST_NO', fea1, fea2, stat]))\n",
    "    new_fea_cols = ['_'.join(['CUST_NO', fea1, fea2, stat, str(col)]) for col in df_tmp.columns]\n",
    "    df_tmp.columns = new_fea_cols\n",
    "    df_tmp.reset_index(inplace=True)\n",
    "        \n",
    "    if stat == 'count':\n",
    "        df_tmp = df_tmp.fillna(0)\n",
    "        \n",
    "    # 去掉全NaN列\n",
    "    valid_cols = []\n",
    "    for col in df_tmp.columns:\n",
    "        if not df_tmp[col].isna().all():\n",
    "            valid_cols.append(col)\n",
    "            \n",
    "    df_fea = df_fea.merge(df_tmp[valid_cols], on='CUST_NO', how='left')\n",
    "    return df_fea, new_fea_cols \n",
    "\n",
    "def get_all_id_category_features(df_fea, df_to_groupby, fea1, fea2, stats):\n",
    "    \"\"\"批量生成类别特征交叉统计\"\"\"\n",
    "    all_new_fea_cols = []\n",
    "    for stat in tqdm(stats, desc=f\"生成{fea1}分组特征\"):\n",
    "        df_fea, new_fea_cols = get_id_category_features(df_fea, df_to_groupby, fea1, fea2, stat)\n",
    "        all_new_fea_cols += new_fea_cols\n",
    "    return df_fea, all_new_fea_cols\n",
    "\n",
    "def get_division_features(df1, df2, col1, col2, eps=1e-6):\n",
    "    \"\"\"生成除法特征(比值特征)\"\"\"\n",
    "    tmp = pd.merge(df1, df2, how=\"left\", on=\"CUST_NO\")\n",
    "    new_feature_name = '_'.join([col1, \"div\", col2])\n",
    "    tmp[new_feature_name] = tmp[col1] / (tmp[col2] + eps)\n",
    "    feature_name = [\"CUST_NO\", new_feature_name]\n",
    "    return tmp[feature_name]\n",
    "\n",
    "print(\"通用函数定义完成\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "3cfee529",
   "metadata": {},
   "source": [
    "## 代发工资信息表特征工程"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "5c6037cc",
   "metadata": {},
   "source": [
    "### 类别特征编码处理"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 28,
   "id": "608eddba",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "开始类别特征编码...\n",
      "单位类型数量: 130\n",
      "省份数量: 5\n",
      "类别特征编码完成\n"
     ]
    }
   ],
   "source": [
    "# 加载代发工资数据\n",
    "aget_pay_df = AGET_PAY_data.copy()\n",
    "\n",
    "# 类别特征编码\n",
    "print(\"开始类别特征编码...\")\n",
    "\n",
    "# 1. 单位类型编码（按频次降序编码）\n",
    "unit_typ_list = aget_pay_df['UNIT_TYP_CD'].value_counts(ascending=False).index\n",
    "unit_typ_dic = {unit_typ_list[i]: i for i, _ in enumerate(unit_typ_list)}\n",
    "print(f\"单位类型数量: {len(unit_typ_dic)}\")\n",
    "\n",
    "# 2. 省份编码（按频次降序编码）\n",
    "prov_cd_list = aget_pay_df['PROV_CD'].value_counts(ascending=False).index\n",
    "prov_cd_dic = {prov_cd_list[i]: i for i, _ in enumerate(prov_cd_list)}\n",
    "print(f\"省份数量: {len(prov_cd_dic)}\")\n",
    "\n",
    "# 3. 应用编码（使用众数填充缺失值）\n",
    "aget_pay_df['PROV_CD'] = aget_pay_df['PROV_CD'].fillna(prov_cd_list[0])\n",
    "aget_pay_df['PROV_CD_encoded'] = aget_pay_df['PROV_CD'].map(prov_cd_dic)\n",
    "\n",
    "aget_pay_df['UNIT_TYP_CD'] = aget_pay_df['UNIT_TYP_CD'].fillna(unit_typ_list[0])\n",
    "aget_pay_df['UNIT_TYP_CD_encoded'] = aget_pay_df['UNIT_TYP_CD'].map(unit_typ_dic)\n",
    "\n",
    "# 4. 金额变换\n",
    "aget_pay_df['TR_AMT_transformed'] = aget_pay_df['TR_AMT'].apply(lambda x: round(pow(x/3.12, 3), 2))\n",
    "\n",
    "print(\"类别特征编码完成\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "921e4dee",
   "metadata": {},
   "source": [
    "### 基础统计特征"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 29,
   "id": "71999558",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "生成基础统计特征...\n",
      "基础统计特征生成完成，特征数: 20\n"
     ]
    },
    {
     "data": {
      "text/html": [
       "<div>\n",
       "<style scoped>\n",
       "    .dataframe tbody tr th:only-of-type {\n",
       "        vertical-align: middle;\n",
       "    }\n",
       "\n",
       "    .dataframe tbody tr th {\n",
       "        vertical-align: top;\n",
       "    }\n",
       "\n",
       "    .dataframe thead th {\n",
       "        text-align: right;\n",
       "    }\n",
       "</style>\n",
       "<table border=\"1\" class=\"dataframe\">\n",
       "  <thead>\n",
       "    <tr style=\"text-align: right;\">\n",
       "      <th></th>\n",
       "      <th>CUST_NO</th>\n",
       "      <th>aget_pay_count</th>\n",
       "      <th>aget_pay_unit_count</th>\n",
       "      <th>tr_amt_sum</th>\n",
       "      <th>tr_amt_mean</th>\n",
       "      <th>tr_amt_std</th>\n",
       "      <th>tr_amt_median</th>\n",
       "      <th>tr_amt_max</th>\n",
       "      <th>tr_amt_min</th>\n",
       "      <th>tr_amt_skew</th>\n",
       "      <th>...</th>\n",
       "      <th>tr_amt_transformed_sum</th>\n",
       "      <th>tr_amt_transformed_mean</th>\n",
       "      <th>tr_amt_transformed_std</th>\n",
       "      <th>prov_cd_mean</th>\n",
       "      <th>prov_cd_nunique</th>\n",
       "      <th>unit_typ_cd_mean</th>\n",
       "      <th>unit_typ_cd_nunique</th>\n",
       "      <th>tr_amt_range</th>\n",
       "      <th>tr_amt_cv</th>\n",
       "      <th>avg_amt_per_unit</th>\n",
       "    </tr>\n",
       "  </thead>\n",
       "  <tbody>\n",
       "    <tr>\n",
       "      <th>0</th>\n",
       "      <td>00c02817eb07e2226f7e2057c1df05ca</td>\n",
       "      <td>8</td>\n",
       "      <td>1</td>\n",
       "      <td>2565.00</td>\n",
       "      <td>320.625000</td>\n",
       "      <td>315.995451</td>\n",
       "      <td>125.00</td>\n",
       "      <td>700.00</td>\n",
       "      <td>30.00</td>\n",
       "      <td>0.598210</td>\n",
       "      <td>...</td>\n",
       "      <td>3.408156e+07</td>\n",
       "      <td>4.260196e+06</td>\n",
       "      <td>5.824222e+06</td>\n",
       "      <td>0.0</td>\n",
       "      <td>1</td>\n",
       "      <td>49.0</td>\n",
       "      <td>1</td>\n",
       "      <td>670.00</td>\n",
       "      <td>0.985561</td>\n",
       "      <td>2564.997435</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>1</th>\n",
       "      <td>01a58f5c4eb4b00f50c7262656da3081</td>\n",
       "      <td>3</td>\n",
       "      <td>1</td>\n",
       "      <td>5964.50</td>\n",
       "      <td>1988.166667</td>\n",
       "      <td>837.590903</td>\n",
       "      <td>2471.75</td>\n",
       "      <td>2471.75</td>\n",
       "      <td>1021.00</td>\n",
       "      <td>-1.732051</td>\n",
       "      <td>...</td>\n",
       "      <td>1.029487e+09</td>\n",
       "      <td>3.431623e+08</td>\n",
       "      <td>2.668383e+08</td>\n",
       "      <td>0.0</td>\n",
       "      <td>1</td>\n",
       "      <td>13.0</td>\n",
       "      <td>1</td>\n",
       "      <td>1450.75</td>\n",
       "      <td>0.421288</td>\n",
       "      <td>5964.494036</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>2</th>\n",
       "      <td>0295cf9860071e2374df941b97e400e9</td>\n",
       "      <td>2</td>\n",
       "      <td>1</td>\n",
       "      <td>9323.48</td>\n",
       "      <td>4661.740000</td>\n",
       "      <td>317.151533</td>\n",
       "      <td>4661.74</td>\n",
       "      <td>4886.00</td>\n",
       "      <td>4437.48</td>\n",
       "      <td>NaN</td>\n",
       "      <td>...</td>\n",
       "      <td>6.717615e+09</td>\n",
       "      <td>3.358808e+09</td>\n",
       "      <td>6.813265e+08</td>\n",
       "      <td>0.0</td>\n",
       "      <td>1</td>\n",
       "      <td>10.0</td>\n",
       "      <td>1</td>\n",
       "      <td>448.52</td>\n",
       "      <td>0.068033</td>\n",
       "      <td>9323.470677</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>3</th>\n",
       "      <td>02c195666ebe760ece005149180eda98</td>\n",
       "      <td>2</td>\n",
       "      <td>1</td>\n",
       "      <td>2940.00</td>\n",
       "      <td>1470.000000</td>\n",
       "      <td>0.000000</td>\n",
       "      <td>1470.00</td>\n",
       "      <td>1470.00</td>\n",
       "      <td>1470.00</td>\n",
       "      <td>NaN</td>\n",
       "      <td>...</td>\n",
       "      <td>2.091791e+08</td>\n",
       "      <td>1.045895e+08</td>\n",
       "      <td>0.000000e+00</td>\n",
       "      <td>0.0</td>\n",
       "      <td>1</td>\n",
       "      <td>3.0</td>\n",
       "      <td>1</td>\n",
       "      <td>0.00</td>\n",
       "      <td>0.000000</td>\n",
       "      <td>2939.997060</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>4</th>\n",
       "      <td>038e5ea4892945f19f92076676a2395a</td>\n",
       "      <td>6</td>\n",
       "      <td>2</td>\n",
       "      <td>27202.81</td>\n",
       "      <td>4533.801667</td>\n",
       "      <td>2435.158146</td>\n",
       "      <td>4052.54</td>\n",
       "      <td>8462.64</td>\n",
       "      <td>2530.00</td>\n",
       "      <td>0.805353</td>\n",
       "      <td>...</td>\n",
       "      <td>3.296564e+10</td>\n",
       "      <td>5.494274e+09</td>\n",
       "      <td>7.523843e+09</td>\n",
       "      <td>0.0</td>\n",
       "      <td>1</td>\n",
       "      <td>6.0</td>\n",
       "      <td>2</td>\n",
       "      <td>5932.64</td>\n",
       "      <td>0.537112</td>\n",
       "      <td>13601.398199</td>\n",
       "    </tr>\n",
       "  </tbody>\n",
       "</table>\n",
       "<p>5 rows × 21 columns</p>\n",
       "</div>"
      ],
      "text/plain": [
       "                            CUST_NO  aget_pay_count  aget_pay_unit_count  \\\n",
       "0  00c02817eb07e2226f7e2057c1df05ca               8                    1   \n",
       "1  01a58f5c4eb4b00f50c7262656da3081               3                    1   \n",
       "2  0295cf9860071e2374df941b97e400e9               2                    1   \n",
       "3  02c195666ebe760ece005149180eda98               2                    1   \n",
       "4  038e5ea4892945f19f92076676a2395a               6                    2   \n",
       "\n",
       "   tr_amt_sum  tr_amt_mean   tr_amt_std  tr_amt_median  tr_amt_max  \\\n",
       "0     2565.00   320.625000   315.995451         125.00      700.00   \n",
       "1     5964.50  1988.166667   837.590903        2471.75     2471.75   \n",
       "2     9323.48  4661.740000   317.151533        4661.74     4886.00   \n",
       "3     2940.00  1470.000000     0.000000        1470.00     1470.00   \n",
       "4    27202.81  4533.801667  2435.158146        4052.54     8462.64   \n",
       "\n",
       "   tr_amt_min  tr_amt_skew  ...  tr_amt_transformed_sum  \\\n",
       "0       30.00     0.598210  ...            3.408156e+07   \n",
       "1     1021.00    -1.732051  ...            1.029487e+09   \n",
       "2     4437.48          NaN  ...            6.717615e+09   \n",
       "3     1470.00          NaN  ...            2.091791e+08   \n",
       "4     2530.00     0.805353  ...            3.296564e+10   \n",
       "\n",
       "   tr_amt_transformed_mean  tr_amt_transformed_std  prov_cd_mean  \\\n",
       "0             4.260196e+06            5.824222e+06           0.0   \n",
       "1             3.431623e+08            2.668383e+08           0.0   \n",
       "2             3.358808e+09            6.813265e+08           0.0   \n",
       "3             1.045895e+08            0.000000e+00           0.0   \n",
       "4             5.494274e+09            7.523843e+09           0.0   \n",
       "\n",
       "   prov_cd_nunique  unit_typ_cd_mean  unit_typ_cd_nunique  tr_amt_range  \\\n",
       "0                1              49.0                    1        670.00   \n",
       "1                1              13.0                    1       1450.75   \n",
       "2                1              10.0                    1        448.52   \n",
       "3                1               3.0                    1          0.00   \n",
       "4                1               6.0                    2       5932.64   \n",
       "\n",
       "   tr_amt_cv  avg_amt_per_unit  \n",
       "0   0.985561       2564.997435  \n",
       "1   0.421288       5964.494036  \n",
       "2   0.068033       9323.470677  \n",
       "3   0.000000       2939.997060  \n",
       "4   0.537112      13601.398199  \n",
       "\n",
       "[5 rows x 21 columns]"
      ]
     },
     "execution_count": 29,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "# 基础统计特征（按客户聚合）\n",
    "print(\"生成基础统计特征...\")\n",
    "\n",
    "aget_pay_basic_features = aget_pay_df.groupby('CUST_NO').agg(\n",
    "    # 代发笔数\n",
    "    aget_pay_count=('AGEN_CUSNO', 'count'),\n",
    "    \n",
    "    # 代发单位数\n",
    "    aget_pay_unit_count=('AGEN_CUSNO', 'nunique'),\n",
    "    \n",
    "    # 金额统计\n",
    "    tr_amt_sum=('TR_AMT', 'sum'),\n",
    "    tr_amt_mean=('TR_AMT', 'mean'),\n",
    "    tr_amt_std=('TR_AMT', 'std'),\n",
    "    tr_amt_median=('TR_AMT', 'median'),\n",
    "    tr_amt_max=('TR_AMT', 'max'),\n",
    "    tr_amt_min=('TR_AMT', 'min'),\n",
    "    tr_amt_skew=('TR_AMT', lambda x: x.skew()),\n",
    "    tr_amt_kurt=('TR_AMT', lambda x: x.kurt()),\n",
    "    \n",
    "    # 变换后金额统计\n",
    "    tr_amt_transformed_sum=('TR_AMT_transformed', 'sum'),\n",
    "    tr_amt_transformed_mean=('TR_AMT_transformed', 'mean'),\n",
    "    tr_amt_transformed_std=('TR_AMT_transformed', 'std'),\n",
    "    \n",
    "    # 类别特征统计\n",
    "    prov_cd_mean=('PROV_CD_encoded', 'mean'),\n",
    "    prov_cd_nunique=('PROV_CD', 'nunique'),\n",
    "    unit_typ_cd_mean=('UNIT_TYP_CD_encoded', 'mean'),\n",
    "    unit_typ_cd_nunique=('UNIT_TYP_CD', 'nunique'),\n",
    ").reset_index()\n",
    "\n",
    "# 派生特征\n",
    "aget_pay_basic_features['tr_amt_range'] = aget_pay_basic_features['tr_amt_max'] - aget_pay_basic_features['tr_amt_min']\n",
    "aget_pay_basic_features['tr_amt_cv'] = aget_pay_basic_features['tr_amt_std'] / (aget_pay_basic_features['tr_amt_mean'] + 1e-6)  # 变异系数\n",
    "aget_pay_basic_features['avg_amt_per_unit'] = aget_pay_basic_features['tr_amt_sum'] / (aget_pay_basic_features['aget_pay_unit_count'] + 1e-6)  # 单位平均金额\n",
    "\n",
    "print(f\"基础统计特征生成完成，特征数: {len(aget_pay_basic_features.columns) - 1}\")\n",
    "aget_pay_basic_features.head()"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "7ef74afd",
   "metadata": {},
   "source": [
    "### 时间序列特征"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 30,
   "id": "dfa14d15",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "生成时间序列特征...\n",
      "时间序列特征生成完成，特征数: 9\n"
     ]
    },
    {
     "data": {
      "text/html": [
       "<div>\n",
       "<style scoped>\n",
       "    .dataframe tbody tr th:only-of-type {\n",
       "        vertical-align: middle;\n",
       "    }\n",
       "\n",
       "    .dataframe tbody tr th {\n",
       "        vertical-align: top;\n",
       "    }\n",
       "\n",
       "    .dataframe thead th {\n",
       "        text-align: right;\n",
       "    }\n",
       "</style>\n",
       "<table border=\"1\" class=\"dataframe\">\n",
       "  <thead>\n",
       "    <tr style=\"text-align: right;\">\n",
       "      <th></th>\n",
       "      <th>CUST_NO</th>\n",
       "      <th>aget_day_diff_max</th>\n",
       "      <th>aget_day_diff_min</th>\n",
       "      <th>aget_day_diff_mean</th>\n",
       "      <th>aget_day_diff_std</th>\n",
       "      <th>aget_day_diff_median</th>\n",
       "      <th>last_aget_days_ago</th>\n",
       "      <th>first_aget_days_ago</th>\n",
       "      <th>aget_day_diff_cv</th>\n",
       "      <th>aget_day_diff_range</th>\n",
       "    </tr>\n",
       "  </thead>\n",
       "  <tbody>\n",
       "    <tr>\n",
       "      <th>0</th>\n",
       "      <td>00c02817eb07e2226f7e2057c1df05ca</td>\n",
       "      <td>33.0</td>\n",
       "      <td>0.0</td>\n",
       "      <td>10.428571</td>\n",
       "      <td>14.638501</td>\n",
       "      <td>3.0</td>\n",
       "      <td>0</td>\n",
       "      <td>66</td>\n",
       "      <td>1.403692</td>\n",
       "      <td>33.0</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>1</th>\n",
       "      <td>01a58f5c4eb4b00f50c7262656da3081</td>\n",
       "      <td>13.0</td>\n",
       "      <td>12.0</td>\n",
       "      <td>12.500000</td>\n",
       "      <td>0.707107</td>\n",
       "      <td>12.5</td>\n",
       "      <td>0</td>\n",
       "      <td>13</td>\n",
       "      <td>0.056569</td>\n",
       "      <td>1.0</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>2</th>\n",
       "      <td>0295cf9860071e2374df941b97e400e9</td>\n",
       "      <td>30.0</td>\n",
       "      <td>30.0</td>\n",
       "      <td>30.000000</td>\n",
       "      <td>NaN</td>\n",
       "      <td>30.0</td>\n",
       "      <td>0</td>\n",
       "      <td>0</td>\n",
       "      <td>NaN</td>\n",
       "      <td>0.0</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>3</th>\n",
       "      <td>02c195666ebe760ece005149180eda98</td>\n",
       "      <td>35.0</td>\n",
       "      <td>35.0</td>\n",
       "      <td>35.000000</td>\n",
       "      <td>NaN</td>\n",
       "      <td>35.0</td>\n",
       "      <td>0</td>\n",
       "      <td>0</td>\n",
       "      <td>NaN</td>\n",
       "      <td>0.0</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>4</th>\n",
       "      <td>038e5ea4892945f19f92076676a2395a</td>\n",
       "      <td>30.0</td>\n",
       "      <td>0.0</td>\n",
       "      <td>17.200000</td>\n",
       "      <td>15.786070</td>\n",
       "      <td>26.0</td>\n",
       "      <td>0</td>\n",
       "      <td>60</td>\n",
       "      <td>0.917795</td>\n",
       "      <td>30.0</td>\n",
       "    </tr>\n",
       "  </tbody>\n",
       "</table>\n",
       "</div>"
      ],
      "text/plain": [
       "                            CUST_NO  aget_day_diff_max  aget_day_diff_min  \\\n",
       "0  00c02817eb07e2226f7e2057c1df05ca               33.0                0.0   \n",
       "1  01a58f5c4eb4b00f50c7262656da3081               13.0               12.0   \n",
       "2  0295cf9860071e2374df941b97e400e9               30.0               30.0   \n",
       "3  02c195666ebe760ece005149180eda98               35.0               35.0   \n",
       "4  038e5ea4892945f19f92076676a2395a               30.0                0.0   \n",
       "\n",
       "   aget_day_diff_mean  aget_day_diff_std  aget_day_diff_median  \\\n",
       "0           10.428571          14.638501                   3.0   \n",
       "1           12.500000           0.707107                  12.5   \n",
       "2           30.000000                NaN                  30.0   \n",
       "3           35.000000                NaN                  35.0   \n",
       "4           17.200000          15.786070                  26.0   \n",
       "\n",
       "   last_aget_days_ago  first_aget_days_ago  aget_day_diff_cv  \\\n",
       "0                   0                   66          1.403692   \n",
       "1                   0                   13          0.056569   \n",
       "2                   0                    0               NaN   \n",
       "3                   0                    0               NaN   \n",
       "4                   0                   60          0.917795   \n",
       "\n",
       "   aget_day_diff_range  \n",
       "0                 33.0  \n",
       "1                  1.0  \n",
       "2                  0.0  \n",
       "3                  0.0  \n",
       "4                 30.0  "
      ]
     },
     "execution_count": 30,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "# 时间序列特征\n",
    "print(\"生成时间序列特征...\")\n",
    "\n",
    "# 日期转换与排序\n",
    "aget_pay_time = aget_pay_df.copy()\n",
    "aget_pay_time['DATE'] = pd.to_datetime(aget_pay_time['DATE'], format='%Y%m%d')\n",
    "aget_pay_time = aget_pay_time.sort_values(['CUST_NO', 'DATE'])\n",
    "\n",
    "# 计算相邻两次代发的间隔天数\n",
    "aget_pay_time['aget_day_diff'] = aget_pay_time.groupby('CUST_NO')['DATE'].diff().dt.days\n",
    "\n",
    "# 代发间隔统计特征\n",
    "aget_pay_time_features = aget_pay_time.dropna(subset=['aget_day_diff']).groupby('CUST_NO').agg(\n",
    "    # 间隔天数统计\n",
    "    aget_day_diff_max=('aget_day_diff', 'max'),\n",
    "    aget_day_diff_min=('aget_day_diff', 'min'),\n",
    "    aget_day_diff_mean=('aget_day_diff', 'mean'),\n",
    "    aget_day_diff_std=('aget_day_diff', 'std'),\n",
    "    aget_day_diff_median=('aget_day_diff', 'median'),\n",
    "    \n",
    "    # 最近一次代发距今天数\n",
    "    last_aget_days_ago=('DATE', lambda x: (x.max() - x.iloc[-1]).days),\n",
    "    \n",
    "    # 首次代发距今天数\n",
    "    first_aget_days_ago=('DATE', lambda x: (x.max() - x.iloc[0]).days),\n",
    ").reset_index()\n",
    "\n",
    "# 派生特征\n",
    "aget_pay_time_features['aget_day_diff_cv'] = aget_pay_time_features['aget_day_diff_std'] / (aget_pay_time_features['aget_day_diff_mean'] + 1e-6)  # 间隔变异系数(规律性)\n",
    "aget_pay_time_features['aget_day_diff_range'] = aget_pay_time_features['aget_day_diff_max'] - aget_pay_time_features['aget_day_diff_min']\n",
    "\n",
    "print(f\"时间序列特征生成完成，特征数: {len(aget_pay_time_features.columns) - 1}\")\n",
    "aget_pay_time_features.head()"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "33da16b8",
   "metadata": {},
   "source": [
    "### 单位维度特征"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 31,
   "id": "a0de479e",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "生成单位维度特征...\n",
      "单位维度特征生成完成，特征数: 4\n"
     ]
    },
    {
     "data": {
      "text/html": [
       "<div>\n",
       "<style scoped>\n",
       "    .dataframe tbody tr th:only-of-type {\n",
       "        vertical-align: middle;\n",
       "    }\n",
       "\n",
       "    .dataframe tbody tr th {\n",
       "        vertical-align: top;\n",
       "    }\n",
       "\n",
       "    .dataframe thead th {\n",
       "        text-align: right;\n",
       "    }\n",
       "</style>\n",
       "<table border=\"1\" class=\"dataframe\">\n",
       "  <thead>\n",
       "    <tr style=\"text-align: right;\">\n",
       "      <th></th>\n",
       "      <th>CUST_NO</th>\n",
       "      <th>main_unit_ratio</th>\n",
       "      <th>main_unit_type_ratio</th>\n",
       "      <th>main_prov_ratio</th>\n",
       "      <th>max_unit_amt_ratio</th>\n",
       "    </tr>\n",
       "  </thead>\n",
       "  <tbody>\n",
       "    <tr>\n",
       "      <th>0</th>\n",
       "      <td>00c02817eb07e2226f7e2057c1df05ca</td>\n",
       "      <td>1.0</td>\n",
       "      <td>1.0</td>\n",
       "      <td>1.0</td>\n",
       "      <td>1.000000</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>1</th>\n",
       "      <td>01a58f5c4eb4b00f50c7262656da3081</td>\n",
       "      <td>1.0</td>\n",
       "      <td>1.0</td>\n",
       "      <td>1.0</td>\n",
       "      <td>1.000000</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>2</th>\n",
       "      <td>0295cf9860071e2374df941b97e400e9</td>\n",
       "      <td>1.0</td>\n",
       "      <td>1.0</td>\n",
       "      <td>1.0</td>\n",
       "      <td>1.000000</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>3</th>\n",
       "      <td>02c195666ebe760ece005149180eda98</td>\n",
       "      <td>1.0</td>\n",
       "      <td>1.0</td>\n",
       "      <td>1.0</td>\n",
       "      <td>1.000000</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>4</th>\n",
       "      <td>038e5ea4892945f19f92076676a2395a</td>\n",
       "      <td>0.5</td>\n",
       "      <td>0.5</td>\n",
       "      <td>1.0</td>\n",
       "      <td>0.720985</td>\n",
       "    </tr>\n",
       "  </tbody>\n",
       "</table>\n",
       "</div>"
      ],
      "text/plain": [
       "                            CUST_NO  main_unit_ratio  main_unit_type_ratio  \\\n",
       "0  00c02817eb07e2226f7e2057c1df05ca              1.0                   1.0   \n",
       "1  01a58f5c4eb4b00f50c7262656da3081              1.0                   1.0   \n",
       "2  0295cf9860071e2374df941b97e400e9              1.0                   1.0   \n",
       "3  02c195666ebe760ece005149180eda98              1.0                   1.0   \n",
       "4  038e5ea4892945f19f92076676a2395a              0.5                   0.5   \n",
       "\n",
       "   main_prov_ratio  max_unit_amt_ratio  \n",
       "0              1.0            1.000000  \n",
       "1              1.0            1.000000  \n",
       "2              1.0            1.000000  \n",
       "3              1.0            1.000000  \n",
       "4              1.0            0.720985  "
      ]
     },
     "execution_count": 31,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "# 单位维度特征\n",
    "print(\"生成单位维度特征...\")\n",
    "\n",
    "# 单位集中度（主要单位占比）\n",
    "def unit_concentration(x):\n",
    "    \"\"\"计算主要单位的代发次数占比\"\"\"\n",
    "    if len(x) == 0:\n",
    "        return 0\n",
    "    return x.value_counts().iloc[0] / len(x)\n",
    "\n",
    "aget_pay_unit_features = aget_pay_df.groupby('CUST_NO').agg(\n",
    "    # 主要单位占比\n",
    "    main_unit_ratio=('AGEN_CUSNO', unit_concentration),\n",
    "    \n",
    "    # 主要单位类型占比\n",
    "    main_unit_type_ratio=('UNIT_TYP_CD', unit_concentration),\n",
    "    \n",
    "    # 主要省份占比\n",
    "    main_prov_ratio=('PROV_CD', unit_concentration),\n",
    ").reset_index()\n",
    "\n",
    "# 单位金额统计\n",
    "unit_amt_stats = aget_pay_df.groupby(['CUST_NO', 'AGEN_CUSNO'])['TR_AMT'].agg(['sum', 'mean', 'count']).reset_index()\n",
    "\n",
    "# 最大代发单位的金额占比\n",
    "max_unit_amt = unit_amt_stats.groupby('CUST_NO').apply(\n",
    "    lambda x: x.loc[x['sum'].idxmax(), 'sum'] / x['sum'].sum() if len(x) > 0 else 0\n",
    ").reset_index()\n",
    "max_unit_amt.columns = ['CUST_NO', 'max_unit_amt_ratio']\n",
    "\n",
    "aget_pay_unit_features = aget_pay_unit_features.merge(max_unit_amt, on='CUST_NO', how='left')\n",
    "\n",
    "print(f\"单位维度特征生成完成，特征数: {len(aget_pay_unit_features.columns) - 1}\")\n",
    "aget_pay_unit_features.head()"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "f51fab97",
   "metadata": {},
   "source": [
    "### 月度特征"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 32,
   "id": "368fc70d",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "生成月度特征...\n",
      "月度特征生成完成，特征数: 13\n"
     ]
    },
    {
     "data": {
      "text/html": [
       "<div>\n",
       "<style scoped>\n",
       "    .dataframe tbody tr th:only-of-type {\n",
       "        vertical-align: middle;\n",
       "    }\n",
       "\n",
       "    .dataframe tbody tr th {\n",
       "        vertical-align: top;\n",
       "    }\n",
       "\n",
       "    .dataframe thead th {\n",
       "        text-align: right;\n",
       "    }\n",
       "</style>\n",
       "<table border=\"1\" class=\"dataframe\">\n",
       "  <thead>\n",
       "    <tr style=\"text-align: right;\">\n",
       "      <th></th>\n",
       "      <th>CUST_NO</th>\n",
       "      <th>active_months</th>\n",
       "      <th>avg_month_count</th>\n",
       "      <th>month_count_std</th>\n",
       "      <th>month_count_max</th>\n",
       "      <th>month_count_min</th>\n",
       "      <th>avg_month_amt</th>\n",
       "      <th>month_amt_std</th>\n",
       "      <th>month_amt_max</th>\n",
       "      <th>month_amt_min</th>\n",
       "      <th>avg_month_unit</th>\n",
       "      <th>month_unit_std</th>\n",
       "      <th>month_count_cv</th>\n",
       "      <th>month_amt_cv</th>\n",
       "    </tr>\n",
       "  </thead>\n",
       "  <tbody>\n",
       "    <tr>\n",
       "      <th>0</th>\n",
       "      <td>00c02817eb07e2226f7e2057c1df05ca</td>\n",
       "      <td>3</td>\n",
       "      <td>2.666667</td>\n",
       "      <td>0.577350</td>\n",
       "      <td>3</td>\n",
       "      <td>2</td>\n",
       "      <td>855.000000</td>\n",
       "      <td>30.000000</td>\n",
       "      <td>885.00</td>\n",
       "      <td>825.00</td>\n",
       "      <td>1.000000</td>\n",
       "      <td>0.00000</td>\n",
       "      <td>0.216506</td>\n",
       "      <td>0.035088</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>1</th>\n",
       "      <td>01a58f5c4eb4b00f50c7262656da3081</td>\n",
       "      <td>2</td>\n",
       "      <td>1.500000</td>\n",
       "      <td>0.707107</td>\n",
       "      <td>2</td>\n",
       "      <td>1</td>\n",
       "      <td>2982.250000</td>\n",
       "      <td>721.956024</td>\n",
       "      <td>3492.75</td>\n",
       "      <td>2471.75</td>\n",
       "      <td>1.000000</td>\n",
       "      <td>0.00000</td>\n",
       "      <td>0.471404</td>\n",
       "      <td>0.242084</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>2</th>\n",
       "      <td>0295cf9860071e2374df941b97e400e9</td>\n",
       "      <td>2</td>\n",
       "      <td>1.000000</td>\n",
       "      <td>0.000000</td>\n",
       "      <td>1</td>\n",
       "      <td>1</td>\n",
       "      <td>4661.740000</td>\n",
       "      <td>317.151533</td>\n",
       "      <td>4886.00</td>\n",
       "      <td>4437.48</td>\n",
       "      <td>1.000000</td>\n",
       "      <td>0.00000</td>\n",
       "      <td>0.000000</td>\n",
       "      <td>0.068033</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>3</th>\n",
       "      <td>02c195666ebe760ece005149180eda98</td>\n",
       "      <td>2</td>\n",
       "      <td>1.000000</td>\n",
       "      <td>0.000000</td>\n",
       "      <td>1</td>\n",
       "      <td>1</td>\n",
       "      <td>1470.000000</td>\n",
       "      <td>0.000000</td>\n",
       "      <td>1470.00</td>\n",
       "      <td>1470.00</td>\n",
       "      <td>1.000000</td>\n",
       "      <td>0.00000</td>\n",
       "      <td>0.000000</td>\n",
       "      <td>0.000000</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>4</th>\n",
       "      <td>038e5ea4892945f19f92076676a2395a</td>\n",
       "      <td>3</td>\n",
       "      <td>2.000000</td>\n",
       "      <td>1.000000</td>\n",
       "      <td>3</td>\n",
       "      <td>1</td>\n",
       "      <td>9067.603333</td>\n",
       "      <td>7068.183617</td>\n",
       "      <td>16567.72</td>\n",
       "      <td>2530.00</td>\n",
       "      <td>1.666667</td>\n",
       "      <td>0.57735</td>\n",
       "      <td>0.500000</td>\n",
       "      <td>0.779499</td>\n",
       "    </tr>\n",
       "  </tbody>\n",
       "</table>\n",
       "</div>"
      ],
      "text/plain": [
       "                            CUST_NO  active_months  avg_month_count  \\\n",
       "0  00c02817eb07e2226f7e2057c1df05ca              3         2.666667   \n",
       "1  01a58f5c4eb4b00f50c7262656da3081              2         1.500000   \n",
       "2  0295cf9860071e2374df941b97e400e9              2         1.000000   \n",
       "3  02c195666ebe760ece005149180eda98              2         1.000000   \n",
       "4  038e5ea4892945f19f92076676a2395a              3         2.000000   \n",
       "\n",
       "   month_count_std  month_count_max  month_count_min  avg_month_amt  \\\n",
       "0         0.577350                3                2     855.000000   \n",
       "1         0.707107                2                1    2982.250000   \n",
       "2         0.000000                1                1    4661.740000   \n",
       "3         0.000000                1                1    1470.000000   \n",
       "4         1.000000                3                1    9067.603333   \n",
       "\n",
       "   month_amt_std  month_amt_max  month_amt_min  avg_month_unit  \\\n",
       "0      30.000000         885.00         825.00        1.000000   \n",
       "1     721.956024        3492.75        2471.75        1.000000   \n",
       "2     317.151533        4886.00        4437.48        1.000000   \n",
       "3       0.000000        1470.00        1470.00        1.000000   \n",
       "4    7068.183617       16567.72        2530.00        1.666667   \n",
       "\n",
       "   month_unit_std  month_count_cv  month_amt_cv  \n",
       "0         0.00000        0.216506      0.035088  \n",
       "1         0.00000        0.471404      0.242084  \n",
       "2         0.00000        0.000000      0.068033  \n",
       "3         0.00000        0.000000      0.000000  \n",
       "4         0.57735        0.500000      0.779499  "
      ]
     },
     "execution_count": 32,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "# 月度特征\n",
    "print(\"生成月度特征...\")\n",
    "\n",
    "# 提取月份\n",
    "aget_pay_month = aget_pay_df.copy()\n",
    "aget_pay_month['month'] = pd.to_datetime(aget_pay_month['DATE'], format='%Y%m%d').dt.month\n",
    "\n",
    "# 按客户和月份聚合\n",
    "aget_pay_month_agg = aget_pay_month.groupby(['CUST_NO', 'month']).agg(\n",
    "    month_count=('AGEN_CUSNO', 'count'),\n",
    "    month_amt_sum=('TR_AMT', 'sum'),\n",
    "    month_amt_mean=('TR_AMT', 'mean'),\n",
    "    month_unit_count=('AGEN_CUSNO', 'nunique'),\n",
    ").reset_index()\n",
    "\n",
    "# 月度趋势特征\n",
    "aget_pay_month_features = aget_pay_month_agg.groupby('CUST_NO').agg(\n",
    "    # 活跃月份数\n",
    "    active_months=('month', 'nunique'),\n",
    "    \n",
    "    # 月均代发次数\n",
    "    avg_month_count=('month_count', 'mean'),\n",
    "    month_count_std=('month_count', 'std'),\n",
    "    month_count_max=('month_count', 'max'),\n",
    "    month_count_min=('month_count', 'min'),\n",
    "    \n",
    "    # 月均代发金额\n",
    "    avg_month_amt=('month_amt_sum', 'mean'),\n",
    "    month_amt_std=('month_amt_sum', 'std'),\n",
    "    month_amt_max=('month_amt_sum', 'max'),\n",
    "    month_amt_min=('month_amt_sum', 'min'),\n",
    "    \n",
    "    # 月均单位数\n",
    "    avg_month_unit=('month_unit_count', 'mean'),\n",
    "    month_unit_std=('month_unit_count', 'std'),\n",
    ").reset_index()\n",
    "\n",
    "# 月度稳定性（变异系数）\n",
    "aget_pay_month_features['month_count_cv'] = aget_pay_month_features['month_count_std'] / (aget_pay_month_features['avg_month_count'] + 1e-6)\n",
    "aget_pay_month_features['month_amt_cv'] = aget_pay_month_features['month_amt_std'] / (aget_pay_month_features['avg_month_amt'] + 1e-6)\n",
    "\n",
    "print(f\"月度特征生成完成，特征数: {len(aget_pay_month_features.columns) - 1}\")\n",
    "aget_pay_month_features.head()"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "079e0ac4",
   "metadata": {},
   "source": [
    "### 合并代发工资所有特征并保存"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 33,
   "id": "696a4593",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "合并所有代发工资特征...\n"
     ]
    }
   ],
   "source": [
    "# 合并所有代发工资特征\n",
    "print(\"合并所有代发工资特征...\")\n",
    "\n",
    "# 逐步合并\n",
    "aget_pay_features_final = aget_pay_basic_features.copy()\n",
    "aget_pay_features_final = aget_pay_features_final.merge(aget_pay_time_features, on='CUST_NO', how='left')\n",
    "aget_pay_features_final = aget_pay_features_final.merge(aget_pay_unit_features, on='CUST_NO', how='left')\n",
    "aget_pay_features_final = aget_pay_features_final.merge(aget_pay_month_features, on='CUST_NO', how='left')"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "b4863a7a",
   "metadata": {},
   "source": [
    "### 训练集保存"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 15,
   "id": "8922148e",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "\n",
      "特征文件已保存: ./feature/Train\\TRAIN_AGET_PAY_features.pkl\n",
      "文件大小: 1.69 MB\n"
     ]
    }
   ],
   "source": [
    "# 确保特征目录存在\n",
    "feature_dir = './feature/Train'\n",
    "if not os.path.exists(feature_dir):\n",
    "    os.makedirs(feature_dir)\n",
    "    print(f\"创建特征目录: {feature_dir}\")\n",
    "\n",
    "# 保存为pickle格式\n",
    "output_file = os.path.join(feature_dir, 'TRAIN_AGET_PAY_features.pkl')\n",
    "with open(output_file, 'wb') as f:\n",
    "    pickle.dump(aget_pay_features_final, f)\n",
    "\n",
    "print(f\"\\n特征文件已保存: {output_file}\")\n",
    "print(f\"文件大小: {os.path.getsize(output_file) / 1024 / 1024:.2f} MB\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "ac0cdaa1",
   "metadata": {},
   "source": [
    "### A测试集"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 34,
   "id": "b5747912",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "\n",
      "特征文件已保存: ./feature/A\\A_AGET_PAY_features.pkl\n",
      "文件大小: 0.21 MB\n"
     ]
    }
   ],
   "source": [
    "# 确保特征目录存在\n",
    "feature_dir = './feature/A'\n",
    "if not os.path.exists(feature_dir):\n",
    "    os.makedirs(feature_dir)\n",
    "    print(f\"创建特征目录: {feature_dir}\")\n",
    "\n",
    "# 保存为pickle格式\n",
    "output_file = os.path.join(feature_dir, 'A_AGET_PAY_features.pkl')\n",
    "with open(output_file, 'wb') as f:\n",
    "    pickle.dump(aget_pay_features_final, f)\n",
    "\n",
    "print(f\"\\n特征文件已保存: {output_file}\")\n",
    "print(f\"文件大小: {os.path.getsize(output_file) / 1024 / 1024:.2f} MB\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "5335fe7c",
   "metadata": {},
   "source": [
    "## 信用卡交易流水表特征工程"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "cfbda378",
   "metadata": {},
   "source": [
    "### 日期转换与时间特征"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 35,
   "id": "7f889dbc",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "开始日期转换...\n",
      "使用日期列: DATE_TR\n",
      "日期转换完成:\n",
      "交易日期范围: 2025-04-01 ~ 2025-06-27\n",
      "距今天数范围: 0 ~ 87\n",
      "月份分布:\n",
      "date_months_to_now\n",
      "0    56\n",
      "1    38\n",
      "2    35\n",
      "Name: count, dtype: int64\n",
      "日期转换完成，数据形状: (129, 14)\n"
     ]
    }
   ],
   "source": [
    "# 加载信用卡交易数据\n",
    "ccd_tr_dtl = CCD_TR_DTL_data.copy()\n",
    "\n",
    "# 日期转换与时间特征\n",
    "print(\"开始日期转换...\")\n",
    "\n",
    "# 根据实际数据字段调整：DATE_TR是日期列\n",
    "date_col = 'DATE_TR'\n",
    "print(f\"使用日期列: {date_col}\")\n",
    "\n",
    "ccd_tr_dtl = get_days_to_now(ccd_tr_dtl, date_col)\n",
    "\n",
    "# 额外的时间特征\n",
    "ccd_tr_dtl['year'] = pd.to_datetime(ccd_tr_dtl[date_col], format='%Y%m%d').dt.year\n",
    "ccd_tr_dtl['month'] = pd.to_datetime(ccd_tr_dtl[date_col], format='%Y%m%d').dt.month\n",
    "ccd_tr_dtl['day'] = pd.to_datetime(ccd_tr_dtl[date_col], format='%Y%m%d').dt.day\n",
    "ccd_tr_dtl['dayofweek'] = pd.to_datetime(ccd_tr_dtl[date_col], format='%Y%m%d').dt.dayofweek  # 0=周一, 6=周日\n",
    "ccd_tr_dtl['is_weekend'] = ccd_tr_dtl['dayofweek'].apply(lambda x: 1 if x >= 5 else 0)  # 周末标识\n",
    "\n",
    "print(f\"日期转换完成，数据形状: {ccd_tr_dtl.shape}\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "568eecc1",
   "metadata": {},
   "source": [
    "### 基础消费统计特征"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 36,
   "id": "bf9dc22b",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "生成基础消费统计特征...\n",
      "使用金额列: AMT_TR\n",
      "基础消费统计特征生成完成，特征数: 19\n"
     ]
    },
    {
     "data": {
      "text/html": [
       "<div>\n",
       "<style scoped>\n",
       "    .dataframe tbody tr th:only-of-type {\n",
       "        vertical-align: middle;\n",
       "    }\n",
       "\n",
       "    .dataframe tbody tr th {\n",
       "        vertical-align: top;\n",
       "    }\n",
       "\n",
       "    .dataframe thead th {\n",
       "        text-align: right;\n",
       "    }\n",
       "</style>\n",
       "<table border=\"1\" class=\"dataframe\">\n",
       "  <thead>\n",
       "    <tr style=\"text-align: right;\">\n",
       "      <th></th>\n",
       "      <th>CUST_NO</th>\n",
       "      <th>ccd_tr_count</th>\n",
       "      <th>ccd_amt_sum</th>\n",
       "      <th>ccd_amt_mean</th>\n",
       "      <th>ccd_amt_std</th>\n",
       "      <th>ccd_amt_median</th>\n",
       "      <th>ccd_amt_max</th>\n",
       "      <th>ccd_amt_min</th>\n",
       "      <th>ccd_amt_skew</th>\n",
       "      <th>ccd_amt_kurt</th>\n",
       "      <th>ccd_amt_q25</th>\n",
       "      <th>ccd_amt_q75</th>\n",
       "      <th>active_days</th>\n",
       "      <th>weekday_count</th>\n",
       "      <th>weekend_count</th>\n",
       "      <th>ccd_amt_range</th>\n",
       "      <th>ccd_amt_cv</th>\n",
       "      <th>ccd_amt_per_trans</th>\n",
       "      <th>ccd_trans_per_day</th>\n",
       "      <th>weekend_ratio</th>\n",
       "    </tr>\n",
       "  </thead>\n",
       "  <tbody>\n",
       "    <tr>\n",
       "      <th>0</th>\n",
       "      <td>064898a7d7d2bc34872e423170f97bfb</td>\n",
       "      <td>12</td>\n",
       "      <td>27996.96</td>\n",
       "      <td>2333.080000</td>\n",
       "      <td>1055.138950</td>\n",
       "      <td>2916.33</td>\n",
       "      <td>2916.33</td>\n",
       "      <td>583.33</td>\n",
       "      <td>-1.326650</td>\n",
       "      <td>-0.325926</td>\n",
       "      <td>2333.08</td>\n",
       "      <td>2916.33</td>\n",
       "      <td>6</td>\n",
       "      <td>5</td>\n",
       "      <td>7</td>\n",
       "      <td>2333.00</td>\n",
       "      <td>0.452252</td>\n",
       "      <td>2333.079806</td>\n",
       "      <td>2.000000</td>\n",
       "      <td>0.583333</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>1</th>\n",
       "      <td>0b3a60d7823eacb570f33592bca838ad</td>\n",
       "      <td>9</td>\n",
       "      <td>3004.00</td>\n",
       "      <td>333.777778</td>\n",
       "      <td>999.833333</td>\n",
       "      <td>0.50</td>\n",
       "      <td>3000.00</td>\n",
       "      <td>0.50</td>\n",
       "      <td>3.000000</td>\n",
       "      <td>9.000000</td>\n",
       "      <td>0.50</td>\n",
       "      <td>0.50</td>\n",
       "      <td>5</td>\n",
       "      <td>9</td>\n",
       "      <td>0</td>\n",
       "      <td>2999.50</td>\n",
       "      <td>2.995506</td>\n",
       "      <td>333.777741</td>\n",
       "      <td>1.800000</td>\n",
       "      <td>0.000000</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>2</th>\n",
       "      <td>2748b8bdd1889567b1755668e79b5ee7</td>\n",
       "      <td>12</td>\n",
       "      <td>41472.00</td>\n",
       "      <td>3456.000000</td>\n",
       "      <td>1885.049360</td>\n",
       "      <td>4497.00</td>\n",
       "      <td>4500.00</td>\n",
       "      <td>330.00</td>\n",
       "      <td>-1.326648</td>\n",
       "      <td>-0.325928</td>\n",
       "      <td>3455.25</td>\n",
       "      <td>4497.75</td>\n",
       "      <td>9</td>\n",
       "      <td>10</td>\n",
       "      <td>2</td>\n",
       "      <td>4170.00</td>\n",
       "      <td>0.545443</td>\n",
       "      <td>3455.999712</td>\n",
       "      <td>1.333333</td>\n",
       "      <td>0.166667</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>3</th>\n",
       "      <td>42c6b970935d3833c31276fe60aba663</td>\n",
       "      <td>5</td>\n",
       "      <td>85332.32</td>\n",
       "      <td>17066.464000</td>\n",
       "      <td>35185.650870</td>\n",
       "      <td>1666.33</td>\n",
       "      <td>80000.00</td>\n",
       "      <td>333.33</td>\n",
       "      <td>2.234555</td>\n",
       "      <td>4.994665</td>\n",
       "      <td>1666.33</td>\n",
       "      <td>1666.33</td>\n",
       "      <td>3</td>\n",
       "      <td>1</td>\n",
       "      <td>4</td>\n",
       "      <td>79666.67</td>\n",
       "      <td>2.061684</td>\n",
       "      <td>17066.460587</td>\n",
       "      <td>1.666666</td>\n",
       "      <td>0.800000</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>4</th>\n",
       "      <td>4544d13ddc128e03327fe7ffac914765</td>\n",
       "      <td>10</td>\n",
       "      <td>130.00</td>\n",
       "      <td>13.000000</td>\n",
       "      <td>9.055385</td>\n",
       "      <td>20.00</td>\n",
       "      <td>20.00</td>\n",
       "      <td>1.00</td>\n",
       "      <td>-0.499411</td>\n",
       "      <td>-2.224845</td>\n",
       "      <td>3.00</td>\n",
       "      <td>20.00</td>\n",
       "      <td>7</td>\n",
       "      <td>9</td>\n",
       "      <td>1</td>\n",
       "      <td>19.00</td>\n",
       "      <td>0.696568</td>\n",
       "      <td>12.999999</td>\n",
       "      <td>1.428571</td>\n",
       "      <td>0.100000</td>\n",
       "    </tr>\n",
       "  </tbody>\n",
       "</table>\n",
       "</div>"
      ],
      "text/plain": [
       "                            CUST_NO  ccd_tr_count  ccd_amt_sum  ccd_amt_mean  \\\n",
       "0  064898a7d7d2bc34872e423170f97bfb            12     27996.96   2333.080000   \n",
       "1  0b3a60d7823eacb570f33592bca838ad             9      3004.00    333.777778   \n",
       "2  2748b8bdd1889567b1755668e79b5ee7            12     41472.00   3456.000000   \n",
       "3  42c6b970935d3833c31276fe60aba663             5     85332.32  17066.464000   \n",
       "4  4544d13ddc128e03327fe7ffac914765            10       130.00     13.000000   \n",
       "\n",
       "    ccd_amt_std  ccd_amt_median  ccd_amt_max  ccd_amt_min  ccd_amt_skew  \\\n",
       "0   1055.138950         2916.33      2916.33       583.33     -1.326650   \n",
       "1    999.833333            0.50      3000.00         0.50      3.000000   \n",
       "2   1885.049360         4497.00      4500.00       330.00     -1.326648   \n",
       "3  35185.650870         1666.33     80000.00       333.33      2.234555   \n",
       "4      9.055385           20.00        20.00         1.00     -0.499411   \n",
       "\n",
       "   ccd_amt_kurt  ccd_amt_q25  ccd_amt_q75  active_days  weekday_count  \\\n",
       "0     -0.325926      2333.08      2916.33            6              5   \n",
       "1      9.000000         0.50         0.50            5              9   \n",
       "2     -0.325928      3455.25      4497.75            9             10   \n",
       "3      4.994665      1666.33      1666.33            3              1   \n",
       "4     -2.224845         3.00        20.00            7              9   \n",
       "\n",
       "   weekend_count  ccd_amt_range  ccd_amt_cv  ccd_amt_per_trans  \\\n",
       "0              7        2333.00    0.452252        2333.079806   \n",
       "1              0        2999.50    2.995506         333.777741   \n",
       "2              2        4170.00    0.545443        3455.999712   \n",
       "3              4       79666.67    2.061684       17066.460587   \n",
       "4              1          19.00    0.696568          12.999999   \n",
       "\n",
       "   ccd_trans_per_day  weekend_ratio  \n",
       "0           2.000000       0.583333  \n",
       "1           1.800000       0.000000  \n",
       "2           1.333333       0.166667  \n",
       "3           1.666666       0.800000  \n",
       "4           1.428571       0.100000  "
      ]
     },
     "execution_count": 36,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "# 基础消费统计特征\n",
    "print(\"生成基础消费统计特征...\")\n",
    "\n",
    "# 金额列是AMT_TR\n",
    "amt_col = 'AMT_TR'\n",
    "print(f\"使用金额列: {amt_col}\")\n",
    "\n",
    "ccd_basic_features = ccd_tr_dtl.groupby('CUST_NO').agg(\n",
    "    # 交易笔数\n",
    "    ccd_tr_count=(amt_col, 'count'),\n",
    "    \n",
    "    # 金额统计\n",
    "    ccd_amt_sum=(amt_col, 'sum'),\n",
    "    ccd_amt_mean=(amt_col, 'mean'),\n",
    "    ccd_amt_std=(amt_col, 'std'),\n",
    "    ccd_amt_median=(amt_col, 'median'),\n",
    "    ccd_amt_max=(amt_col, 'max'),\n",
    "    ccd_amt_min=(amt_col, 'min'),\n",
    "    ccd_amt_skew=(amt_col, lambda x: x.skew()),\n",
    "    ccd_amt_kurt=(amt_col, lambda x: x.kurt()),\n",
    "    ccd_amt_q25=(amt_col, lambda x: x.quantile(0.25)),\n",
    "    ccd_amt_q75=(amt_col, lambda x: x.quantile(0.75)),\n",
    "    \n",
    "    # 活跃天数\n",
    "    active_days=('date_days_to_now', 'nunique'),\n",
    "    \n",
    "    # 工作日/周末交易\n",
    "    weekday_count=('is_weekend', lambda x: (x == 0).sum()),\n",
    "    weekend_count=('is_weekend', lambda x: (x == 1).sum()),\n",
    ").reset_index()\n",
    "\n",
    "# 派生特征\n",
    "ccd_basic_features['ccd_amt_range'] = ccd_basic_features['ccd_amt_max'] - ccd_basic_features['ccd_amt_min']\n",
    "ccd_basic_features['ccd_amt_cv'] = ccd_basic_features['ccd_amt_std'] / (ccd_basic_features['ccd_amt_mean'] + 1e-6)  # 变异系数\n",
    "ccd_basic_features['ccd_amt_per_trans'] = ccd_basic_features['ccd_amt_sum'] / (ccd_basic_features['ccd_tr_count'] + 1e-6)  # 笔均金额\n",
    "ccd_basic_features['ccd_trans_per_day'] = ccd_basic_features['ccd_tr_count'] / (ccd_basic_features['active_days'] + 1e-6)  # 日均笔数\n",
    "ccd_basic_features['weekend_ratio'] = ccd_basic_features['weekend_count'] / (ccd_basic_features['ccd_tr_count'] + 1e-6)  # 周末交易占比\n",
    "\n",
    "print(f\"基础消费统计特征生成完成，特征数: {len(ccd_basic_features.columns) - 1}\")\n",
    "ccd_basic_features.head()"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "e9f040b4",
   "metadata": {},
   "source": [
    "### 时间序列特征"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 37,
   "id": "59ef787d",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "生成时间序列特征...\n",
      "时间序列特征生成完成，特征数: 9\n"
     ]
    },
    {
     "data": {
      "text/html": [
       "<div>\n",
       "<style scoped>\n",
       "    .dataframe tbody tr th:only-of-type {\n",
       "        vertical-align: middle;\n",
       "    }\n",
       "\n",
       "    .dataframe tbody tr th {\n",
       "        vertical-align: top;\n",
       "    }\n",
       "\n",
       "    .dataframe thead th {\n",
       "        text-align: right;\n",
       "    }\n",
       "</style>\n",
       "<table border=\"1\" class=\"dataframe\">\n",
       "  <thead>\n",
       "    <tr style=\"text-align: right;\">\n",
       "      <th></th>\n",
       "      <th>CUST_NO</th>\n",
       "      <th>last_tr_days_ago</th>\n",
       "      <th>first_tr_days_ago</th>\n",
       "      <th>tr_interval_mean</th>\n",
       "      <th>tr_interval_std</th>\n",
       "      <th>tr_interval_max</th>\n",
       "      <th>tr_interval_min</th>\n",
       "      <th>tr_interval_median</th>\n",
       "      <th>tr_interval_cv</th>\n",
       "      <th>tr_duration_days</th>\n",
       "    </tr>\n",
       "  </thead>\n",
       "  <tbody>\n",
       "    <tr>\n",
       "      <th>0</th>\n",
       "      <td>064898a7d7d2bc34872e423170f97bfb</td>\n",
       "      <td>9</td>\n",
       "      <td>87</td>\n",
       "      <td>7.090909</td>\n",
       "      <td>8.300055</td>\n",
       "      <td>18.0</td>\n",
       "      <td>0.0</td>\n",
       "      <td>0.0</td>\n",
       "      <td>1.170520</td>\n",
       "      <td>78</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>1</th>\n",
       "      <td>0b3a60d7823eacb570f33592bca838ad</td>\n",
       "      <td>64</td>\n",
       "      <td>87</td>\n",
       "      <td>2.875000</td>\n",
       "      <td>6.937218</td>\n",
       "      <td>20.0</td>\n",
       "      <td>0.0</td>\n",
       "      <td>0.5</td>\n",
       "      <td>2.412945</td>\n",
       "      <td>23</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>2</th>\n",
       "      <td>2748b8bdd1889567b1755668e79b5ee7</td>\n",
       "      <td>8</td>\n",
       "      <td>87</td>\n",
       "      <td>7.181818</td>\n",
       "      <td>8.072400</td>\n",
       "      <td>19.0</td>\n",
       "      <td>0.0</td>\n",
       "      <td>1.0</td>\n",
       "      <td>1.124005</td>\n",
       "      <td>79</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>3</th>\n",
       "      <td>42c6b970935d3833c31276fe60aba663</td>\n",
       "      <td>6</td>\n",
       "      <td>28</td>\n",
       "      <td>5.500000</td>\n",
       "      <td>9.712535</td>\n",
       "      <td>20.0</td>\n",
       "      <td>0.0</td>\n",
       "      <td>1.0</td>\n",
       "      <td>1.765915</td>\n",
       "      <td>22</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>4</th>\n",
       "      <td>4544d13ddc128e03327fe7ffac914765</td>\n",
       "      <td>0</td>\n",
       "      <td>87</td>\n",
       "      <td>9.666667</td>\n",
       "      <td>13.820275</td>\n",
       "      <td>30.0</td>\n",
       "      <td>0.0</td>\n",
       "      <td>1.0</td>\n",
       "      <td>1.429683</td>\n",
       "      <td>87</td>\n",
       "    </tr>\n",
       "  </tbody>\n",
       "</table>\n",
       "</div>"
      ],
      "text/plain": [
       "                            CUST_NO  last_tr_days_ago  first_tr_days_ago  \\\n",
       "0  064898a7d7d2bc34872e423170f97bfb                 9                 87   \n",
       "1  0b3a60d7823eacb570f33592bca838ad                64                 87   \n",
       "2  2748b8bdd1889567b1755668e79b5ee7                 8                 87   \n",
       "3  42c6b970935d3833c31276fe60aba663                 6                 28   \n",
       "4  4544d13ddc128e03327fe7ffac914765                 0                 87   \n",
       "\n",
       "   tr_interval_mean  tr_interval_std  tr_interval_max  tr_interval_min  \\\n",
       "0          7.090909         8.300055             18.0              0.0   \n",
       "1          2.875000         6.937218             20.0              0.0   \n",
       "2          7.181818         8.072400             19.0              0.0   \n",
       "3          5.500000         9.712535             20.0              0.0   \n",
       "4          9.666667        13.820275             30.0              0.0   \n",
       "\n",
       "   tr_interval_median  tr_interval_cv  tr_duration_days  \n",
       "0                 0.0        1.170520                78  \n",
       "1                 0.5        2.412945                23  \n",
       "2                 1.0        1.124005                79  \n",
       "3                 1.0        1.765915                22  \n",
       "4                 1.0        1.429683                87  "
      ]
     },
     "execution_count": 37,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "# 时间序列特征\n",
    "print(\"生成时间序列特征...\")\n",
    "\n",
    "# 按客户和日期排序\n",
    "ccd_time = ccd_tr_dtl.sort_values(['CUST_NO', 'date']).copy()\n",
    "\n",
    "# 计算交易间隔\n",
    "ccd_time['tr_interval'] = ccd_time.groupby('CUST_NO')['date'].diff().dt.days\n",
    "\n",
    "ccd_time_features = ccd_time.groupby('CUST_NO').agg(\n",
    "    # 最近一笔交易距今天数\n",
    "    last_tr_days_ago=('date_days_to_now', 'min'),\n",
    "    \n",
    "    # 首笔交易距今天数\n",
    "    first_tr_days_ago=('date_days_to_now', 'max'),\n",
    "    \n",
    "    # 交易间隔统计\n",
    "    tr_interval_mean=('tr_interval', 'mean'),\n",
    "    tr_interval_std=('tr_interval', 'std'),\n",
    "    tr_interval_max=('tr_interval', 'max'),\n",
    "    tr_interval_min=('tr_interval', 'min'),\n",
    "    tr_interval_median=('tr_interval', 'median'),\n",
    ").reset_index()\n",
    "\n",
    "# 派生特征\n",
    "ccd_time_features['tr_interval_cv'] = ccd_time_features['tr_interval_std'] / (ccd_time_features['tr_interval_mean'] + 1e-6)  # 间隔变异系数（规律性）\n",
    "ccd_time_features['tr_duration_days'] = ccd_time_features['first_tr_days_ago'] - ccd_time_features['last_tr_days_ago']  # 交易跨度天数\n",
    "\n",
    "print(f\"时间序列特征生成完成，特征数: {len(ccd_time_features.columns) - 1}\")\n",
    "ccd_time_features.head()"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "a49c734d",
   "metadata": {},
   "source": [
    "### 消费金额分布特征"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 38,
   "id": "dc0d3c9d",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "生成消费金额分布特征...\n",
      "消费金额分布特征生成完成，特征数: 5\n"
     ]
    },
    {
     "data": {
      "text/html": [
       "<div>\n",
       "<style scoped>\n",
       "    .dataframe tbody tr th:only-of-type {\n",
       "        vertical-align: middle;\n",
       "    }\n",
       "\n",
       "    .dataframe tbody tr th {\n",
       "        vertical-align: top;\n",
       "    }\n",
       "\n",
       "    .dataframe thead th {\n",
       "        text-align: right;\n",
       "    }\n",
       "</style>\n",
       "<table border=\"1\" class=\"dataframe\">\n",
       "  <thead>\n",
       "    <tr style=\"text-align: right;\">\n",
       "      <th></th>\n",
       "      <th>CUST_NO</th>\n",
       "      <th>large_trans_count</th>\n",
       "      <th>large_trans_ratio</th>\n",
       "      <th>small_trans_count</th>\n",
       "      <th>small_trans_ratio</th>\n",
       "      <th>top3_amt_ratio</th>\n",
       "    </tr>\n",
       "  </thead>\n",
       "  <tbody>\n",
       "    <tr>\n",
       "      <th>0</th>\n",
       "      <td>064898a7d7d2bc34872e423170f97bfb</td>\n",
       "      <td>0.0</td>\n",
       "      <td>0.000000</td>\n",
       "      <td>3.0</td>\n",
       "      <td>0.250000</td>\n",
       "      <td>0.312498</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>1</th>\n",
       "      <td>0b3a60d7823eacb570f33592bca838ad</td>\n",
       "      <td>1.0</td>\n",
       "      <td>0.111111</td>\n",
       "      <td>8.0</td>\n",
       "      <td>0.888889</td>\n",
       "      <td>0.999001</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>2</th>\n",
       "      <td>2748b8bdd1889567b1755668e79b5ee7</td>\n",
       "      <td>0.0</td>\n",
       "      <td>0.000000</td>\n",
       "      <td>3.0</td>\n",
       "      <td>0.250000</td>\n",
       "      <td>0.325521</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>3</th>\n",
       "      <td>42c6b970935d3833c31276fe60aba663</td>\n",
       "      <td>0.0</td>\n",
       "      <td>0.000000</td>\n",
       "      <td>4.0</td>\n",
       "      <td>0.800000</td>\n",
       "      <td>0.976566</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>4</th>\n",
       "      <td>4544d13ddc128e03327fe7ffac914765</td>\n",
       "      <td>0.0</td>\n",
       "      <td>0.000000</td>\n",
       "      <td>4.0</td>\n",
       "      <td>0.400000</td>\n",
       "      <td>0.461538</td>\n",
       "    </tr>\n",
       "  </tbody>\n",
       "</table>\n",
       "</div>"
      ],
      "text/plain": [
       "                            CUST_NO  large_trans_count  large_trans_ratio  \\\n",
       "0  064898a7d7d2bc34872e423170f97bfb                0.0           0.000000   \n",
       "1  0b3a60d7823eacb570f33592bca838ad                1.0           0.111111   \n",
       "2  2748b8bdd1889567b1755668e79b5ee7                0.0           0.000000   \n",
       "3  42c6b970935d3833c31276fe60aba663                0.0           0.000000   \n",
       "4  4544d13ddc128e03327fe7ffac914765                0.0           0.000000   \n",
       "\n",
       "   small_trans_count  small_trans_ratio  top3_amt_ratio  \n",
       "0                3.0           0.250000        0.312498  \n",
       "1                8.0           0.888889        0.999001  \n",
       "2                3.0           0.250000        0.325521  \n",
       "3                4.0           0.800000        0.976566  \n",
       "4                4.0           0.400000        0.461538  "
      ]
     },
     "execution_count": 38,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "# 消费金额分布特征\n",
    "print(\"生成消费金额分布特征...\")\n",
    "\n",
    "# 大额/小额交易定义\n",
    "def amount_distribution_features(group):\n",
    "    \"\"\"计算金额分布相关特征\"\"\"\n",
    "    amt = group[amt_col]\n",
    "    mean_amt = amt.mean()\n",
    "    std_amt = amt.std()\n",
    "    \n",
    "    # 大额交易（>均值+2std）\n",
    "    large_threshold = mean_amt + 2 * std_amt\n",
    "    large_count = (amt > large_threshold).sum()\n",
    "    large_ratio = large_count / len(amt) if len(amt) > 0 else 0\n",
    "    \n",
    "    # 小额交易（<均值）\n",
    "    small_count = (amt < mean_amt).sum()\n",
    "    small_ratio = small_count / len(amt) if len(amt) > 0 else 0\n",
    "    \n",
    "    # Top3金额集中度\n",
    "    top3_sum = amt.nlargest(min(3, len(amt))).sum()\n",
    "    top3_ratio = top3_sum / amt.sum() if amt.sum() > 0 else 0\n",
    "    \n",
    "    return pd.Series({\n",
    "        'large_trans_count': large_count,\n",
    "        'large_trans_ratio': large_ratio,\n",
    "        'small_trans_count': small_count,\n",
    "        'small_trans_ratio': small_ratio,\n",
    "        'top3_amt_ratio': top3_ratio,\n",
    "    })\n",
    "\n",
    "ccd_dist_features = ccd_tr_dtl.groupby('CUST_NO').apply(amount_distribution_features).reset_index()\n",
    "\n",
    "print(f\"消费金额分布特征生成完成，特征数: {len(ccd_dist_features.columns) - 1}\")\n",
    "ccd_dist_features.head()"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "d2315eac",
   "metadata": {},
   "source": [
    "### 月度趋势特征"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 39,
   "id": "634aa2b6",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "生成月度趋势特征...\n",
      "月度趋势特征生成完成，特征数: 12\n"
     ]
    },
    {
     "data": {
      "text/html": [
       "<div>\n",
       "<style scoped>\n",
       "    .dataframe tbody tr th:only-of-type {\n",
       "        vertical-align: middle;\n",
       "    }\n",
       "\n",
       "    .dataframe tbody tr th {\n",
       "        vertical-align: top;\n",
       "    }\n",
       "\n",
       "    .dataframe thead th {\n",
       "        text-align: right;\n",
       "    }\n",
       "</style>\n",
       "<table border=\"1\" class=\"dataframe\">\n",
       "  <thead>\n",
       "    <tr style=\"text-align: right;\">\n",
       "      <th></th>\n",
       "      <th>CUST_NO</th>\n",
       "      <th>active_months</th>\n",
       "      <th>avg_month_count</th>\n",
       "      <th>month_count_std</th>\n",
       "      <th>month_count_max</th>\n",
       "      <th>month_count_min</th>\n",
       "      <th>avg_month_amt</th>\n",
       "      <th>month_amt_std</th>\n",
       "      <th>month_amt_max</th>\n",
       "      <th>month_amt_min</th>\n",
       "      <th>month_count_cv</th>\n",
       "      <th>month_amt_cv</th>\n",
       "      <th>amt_growth_rate</th>\n",
       "    </tr>\n",
       "  </thead>\n",
       "  <tbody>\n",
       "    <tr>\n",
       "      <th>0</th>\n",
       "      <td>064898a7d7d2bc34872e423170f97bfb</td>\n",
       "      <td>3</td>\n",
       "      <td>4.000000</td>\n",
       "      <td>0.00000</td>\n",
       "      <td>4</td>\n",
       "      <td>4</td>\n",
       "      <td>9332.320000</td>\n",
       "      <td>0.00000</td>\n",
       "      <td>9332.32</td>\n",
       "      <td>9332.32</td>\n",
       "      <td>0.000000</td>\n",
       "      <td>0.000000</td>\n",
       "      <td>0.000000</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>1</th>\n",
       "      <td>0b3a60d7823eacb570f33592bca838ad</td>\n",
       "      <td>1</td>\n",
       "      <td>9.000000</td>\n",
       "      <td>NaN</td>\n",
       "      <td>9</td>\n",
       "      <td>9</td>\n",
       "      <td>3004.000000</td>\n",
       "      <td>NaN</td>\n",
       "      <td>3004.00</td>\n",
       "      <td>3004.00</td>\n",
       "      <td>NaN</td>\n",
       "      <td>NaN</td>\n",
       "      <td>NaN</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>2</th>\n",
       "      <td>2748b8bdd1889567b1755668e79b5ee7</td>\n",
       "      <td>3</td>\n",
       "      <td>4.000000</td>\n",
       "      <td>0.00000</td>\n",
       "      <td>4</td>\n",
       "      <td>4</td>\n",
       "      <td>13824.000000</td>\n",
       "      <td>0.00000</td>\n",
       "      <td>13824.00</td>\n",
       "      <td>13824.00</td>\n",
       "      <td>0.000000</td>\n",
       "      <td>0.000000</td>\n",
       "      <td>0.000000</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>3</th>\n",
       "      <td>42c6b970935d3833c31276fe60aba663</td>\n",
       "      <td>1</td>\n",
       "      <td>5.000000</td>\n",
       "      <td>NaN</td>\n",
       "      <td>5</td>\n",
       "      <td>5</td>\n",
       "      <td>85332.320000</td>\n",
       "      <td>NaN</td>\n",
       "      <td>85332.32</td>\n",
       "      <td>85332.32</td>\n",
       "      <td>NaN</td>\n",
       "      <td>NaN</td>\n",
       "      <td>NaN</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>4</th>\n",
       "      <td>4544d13ddc128e03327fe7ffac914765</td>\n",
       "      <td>3</td>\n",
       "      <td>3.333333</td>\n",
       "      <td>0.57735</td>\n",
       "      <td>4</td>\n",
       "      <td>3</td>\n",
       "      <td>43.333333</td>\n",
       "      <td>0.57735</td>\n",
       "      <td>44.00</td>\n",
       "      <td>43.00</td>\n",
       "      <td>0.173205</td>\n",
       "      <td>0.013323</td>\n",
       "      <td>0.023256</td>\n",
       "    </tr>\n",
       "  </tbody>\n",
       "</table>\n",
       "</div>"
      ],
      "text/plain": [
       "                            CUST_NO  active_months  avg_month_count  \\\n",
       "0  064898a7d7d2bc34872e423170f97bfb              3         4.000000   \n",
       "1  0b3a60d7823eacb570f33592bca838ad              1         9.000000   \n",
       "2  2748b8bdd1889567b1755668e79b5ee7              3         4.000000   \n",
       "3  42c6b970935d3833c31276fe60aba663              1         5.000000   \n",
       "4  4544d13ddc128e03327fe7ffac914765              3         3.333333   \n",
       "\n",
       "   month_count_std  month_count_max  month_count_min  avg_month_amt  \\\n",
       "0          0.00000                4                4    9332.320000   \n",
       "1              NaN                9                9    3004.000000   \n",
       "2          0.00000                4                4   13824.000000   \n",
       "3              NaN                5                5   85332.320000   \n",
       "4          0.57735                4                3      43.333333   \n",
       "\n",
       "   month_amt_std  month_amt_max  month_amt_min  month_count_cv  month_amt_cv  \\\n",
       "0        0.00000        9332.32        9332.32        0.000000      0.000000   \n",
       "1            NaN        3004.00        3004.00             NaN           NaN   \n",
       "2        0.00000       13824.00       13824.00        0.000000      0.000000   \n",
       "3            NaN       85332.32       85332.32             NaN           NaN   \n",
       "4        0.57735          44.00          43.00        0.173205      0.013323   \n",
       "\n",
       "   amt_growth_rate  \n",
       "0         0.000000  \n",
       "1              NaN  \n",
       "2         0.000000  \n",
       "3              NaN  \n",
       "4         0.023256  "
      ]
     },
     "execution_count": 39,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "# 月度趋势特征\n",
    "print(\"生成月度趋势特征...\")\n",
    "\n",
    "# 按客户和月份聚合\n",
    "ccd_month_agg = ccd_tr_dtl.groupby(['CUST_NO', 'date_months_to_now']).agg(\n",
    "    month_count=(amt_col, 'count'),\n",
    "    month_amt_sum=(amt_col, 'sum'),\n",
    "    month_amt_mean=(amt_col, 'mean'),\n",
    ").reset_index()\n",
    "\n",
    "# 跨月特征\n",
    "ccd_month_features = ccd_month_agg.groupby('CUST_NO').agg(\n",
    "    # 活跃月份数\n",
    "    active_months=('date_months_to_now', 'nunique'),\n",
    "    \n",
    "    # 月均交易笔数\n",
    "    avg_month_count=('month_count', 'mean'),\n",
    "    month_count_std=('month_count', 'std'),\n",
    "    month_count_max=('month_count', 'max'),\n",
    "    month_count_min=('month_count', 'min'),\n",
    "    \n",
    "    # 月均交易金额\n",
    "    avg_month_amt=('month_amt_sum', 'mean'),\n",
    "    month_amt_std=('month_amt_sum', 'std'),\n",
    "    month_amt_max=('month_amt_sum', 'max'),\n",
    "    month_amt_min=('month_amt_sum', 'min'),\n",
    ").reset_index()\n",
    "\n",
    "# 月度稳定性\n",
    "ccd_month_features['month_count_cv'] = ccd_month_features['month_count_std'] / (ccd_month_features['avg_month_count'] + 1e-6)\n",
    "ccd_month_features['month_amt_cv'] = ccd_month_features['month_amt_std'] / (ccd_month_features['avg_month_amt'] + 1e-6)\n",
    "\n",
    "# 计算月度增长率（最近1个月vs最近2个月）\n",
    "recent_months = ccd_month_agg[ccd_month_agg['date_months_to_now'].isin([0, 1])]\n",
    "if len(recent_months) > 0:\n",
    "    month_growth = recent_months.pivot_table(index='CUST_NO', columns='date_months_to_now', values='month_amt_sum', aggfunc='sum')\n",
    "    if 0 in month_growth.columns and 1 in month_growth.columns:\n",
    "        month_growth['amt_growth_rate'] = (month_growth[0] - month_growth[1]) / (month_growth[1] + 1e-6)\n",
    "        month_growth = month_growth[['amt_growth_rate']].reset_index()\n",
    "        ccd_month_features = ccd_month_features.merge(month_growth, on='CUST_NO', how='left')\n",
    "\n",
    "print(f\"月度趋势特征生成完成，特征数: {len(ccd_month_features.columns) - 1}\")\n",
    "ccd_month_features.head()"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "29a89942",
   "metadata": {},
   "source": [
    "### 交易类型维度特征"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 40,
   "id": "cc0d882c",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "生成商户/交易类型维度特征...\n",
      "使用类别字段: ['COD_PSG', 'COD_TR']\n",
      "商户/交易类型特征生成完成，特征数: 2\n"
     ]
    },
    {
     "data": {
      "text/html": [
       "<div>\n",
       "<style scoped>\n",
       "    .dataframe tbody tr th:only-of-type {\n",
       "        vertical-align: middle;\n",
       "    }\n",
       "\n",
       "    .dataframe tbody tr th {\n",
       "        vertical-align: top;\n",
       "    }\n",
       "\n",
       "    .dataframe thead th {\n",
       "        text-align: right;\n",
       "    }\n",
       "</style>\n",
       "<table border=\"1\" class=\"dataframe\">\n",
       "  <thead>\n",
       "    <tr style=\"text-align: right;\">\n",
       "      <th></th>\n",
       "      <th>CUST_NO</th>\n",
       "      <th>category_count</th>\n",
       "      <th>main_category_ratio</th>\n",
       "    </tr>\n",
       "  </thead>\n",
       "  <tbody>\n",
       "    <tr>\n",
       "      <th>0</th>\n",
       "      <td>064898a7d7d2bc34872e423170f97bfb</td>\n",
       "      <td>2</td>\n",
       "      <td>0.750000</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>1</th>\n",
       "      <td>0b3a60d7823eacb570f33592bca838ad</td>\n",
       "      <td>2</td>\n",
       "      <td>0.888889</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>2</th>\n",
       "      <td>2748b8bdd1889567b1755668e79b5ee7</td>\n",
       "      <td>2</td>\n",
       "      <td>0.750000</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>3</th>\n",
       "      <td>42c6b970935d3833c31276fe60aba663</td>\n",
       "      <td>3</td>\n",
       "      <td>0.600000</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>4</th>\n",
       "      <td>4544d13ddc128e03327fe7ffac914765</td>\n",
       "      <td>2</td>\n",
       "      <td>0.900000</td>\n",
       "    </tr>\n",
       "  </tbody>\n",
       "</table>\n",
       "</div>"
      ],
      "text/plain": [
       "                            CUST_NO  category_count  main_category_ratio\n",
       "0  064898a7d7d2bc34872e423170f97bfb               2             0.750000\n",
       "1  0b3a60d7823eacb570f33592bca838ad               2             0.888889\n",
       "2  2748b8bdd1889567b1755668e79b5ee7               2             0.750000\n",
       "3  42c6b970935d3833c31276fe60aba663               3             0.600000\n",
       "4  4544d13ddc128e03327fe7ffac914765               2             0.900000"
      ]
     },
     "execution_count": 40,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "# 商户/交易类型维度特征\n",
    "print(\"生成商户/交易类型维度特征...\")\n",
    "\n",
    "# COD_PSG是通道代码, COD_TR是交易代码\n",
    "category_cols = ['COD_PSG', 'COD_TR']\n",
    "print(f\"使用类别字段: {category_cols}\")\n",
    "\n",
    "if len(category_cols) > 0:\n",
    "    # 使用第一个类别字段作为示例\n",
    "    cat_col = category_cols[0]\n",
    "    \n",
    "    # 类别多样性\n",
    "    def category_concentration(x):\n",
    "        \"\"\"计算类别集中度\"\"\"\n",
    "        if len(x) == 0:\n",
    "            return 0\n",
    "        return x.value_counts().iloc[0] / len(x)\n",
    "    \n",
    "    ccd_category_features = ccd_tr_dtl.groupby('CUST_NO').agg(\n",
    "        category_count=(cat_col, 'nunique'),  # 类别多样性\n",
    "        main_category_ratio=(cat_col, category_concentration),  # 主类别占比\n",
    "    ).reset_index()\n",
    "    \n",
    "    print(f\"商户/交易类型特征生成完成，特征数: {len(ccd_category_features.columns) - 1}\")\n",
    "else:\n",
    "    # 如果没有类别字段，创建空特征\n",
    "    ccd_category_features = ccd_tr_dtl[['CUST_NO']].drop_duplicates().reset_index(drop=True)\n",
    "    print(\"未检测到商户/交易类型字段，跳过此类特征\")\n",
    "\n",
    "ccd_category_features.head()"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "98af901a",
   "metadata": {},
   "source": [
    "### 合并信用卡交易所有特征并保存"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 41,
   "id": "1313af0b",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "合并所有信用卡交易特征...\n"
     ]
    }
   ],
   "source": [
    "# 合并所有信用卡交易特征\n",
    "print(\"合并所有信用卡交易特征...\")\n",
    "\n",
    "# 逐步合并\n",
    "ccd_features_final = ccd_basic_features.copy()\n",
    "ccd_features_final = ccd_features_final.merge(ccd_time_features, on='CUST_NO', how='left')\n",
    "ccd_features_final = ccd_features_final.merge(ccd_dist_features, on='CUST_NO', how='left')\n",
    "ccd_features_final = ccd_features_final.merge(ccd_month_features, on='CUST_NO', how='left')\n",
    "ccd_features_final = ccd_features_final.merge(ccd_category_features, on='CUST_NO', how='left')"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 25,
   "id": "ce3b922d",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "<class 'pandas.core.frame.DataFrame'>\n",
      "RangeIndex: 169 entries, 0 to 168\n",
      "Data columns (total 48 columns):\n",
      " #   Column               Non-Null Count  Dtype  \n",
      "---  ------               --------------  -----  \n",
      " 0   CUST_NO              169 non-null    object \n",
      " 1   ccd_tr_count         169 non-null    int64  \n",
      " 2   ccd_amt_sum          169 non-null    float64\n",
      " 3   ccd_amt_mean         169 non-null    float64\n",
      " 4   ccd_amt_std          128 non-null    float64\n",
      " 5   ccd_amt_median       169 non-null    float64\n",
      " 6   ccd_amt_max          169 non-null    float64\n",
      " 7   ccd_amt_min          169 non-null    float64\n",
      " 8   ccd_amt_skew         120 non-null    float64\n",
      " 9   ccd_amt_kurt         106 non-null    float64\n",
      " 10  ccd_amt_q25          169 non-null    float64\n",
      " 11  ccd_amt_q75          169 non-null    float64\n",
      " 12  active_days          169 non-null    int64  \n",
      " 13  weekday_count        169 non-null    int64  \n",
      " 14  weekend_count        169 non-null    int64  \n",
      " 15  ccd_amt_range        169 non-null    float64\n",
      " 16  ccd_amt_cv           128 non-null    float64\n",
      " 17  ccd_amt_per_trans    169 non-null    float64\n",
      " 18  ccd_trans_per_day    169 non-null    float64\n",
      " 19  weekend_ratio        169 non-null    float64\n",
      " 20  last_tr_days_ago     169 non-null    int64  \n",
      " 21  first_tr_days_ago    169 non-null    int64  \n",
      " 22  tr_interval_mean     128 non-null    float64\n",
      " 23  tr_interval_std      120 non-null    float64\n",
      " 24  tr_interval_max      128 non-null    float64\n",
      " 25  tr_interval_min      128 non-null    float64\n",
      " 26  tr_interval_median   128 non-null    float64\n",
      " 27  tr_interval_cv       120 non-null    float64\n",
      " 28  tr_duration_days     169 non-null    int64  \n",
      " 29  large_trans_count    169 non-null    float64\n",
      " 30  large_trans_ratio    169 non-null    float64\n",
      " 31  small_trans_count    169 non-null    float64\n",
      " 32  small_trans_ratio    169 non-null    float64\n",
      " 33  top3_amt_ratio       169 non-null    float64\n",
      " 34  active_months        169 non-null    int64  \n",
      " 35  avg_month_count      169 non-null    float64\n",
      " 36  month_count_std      118 non-null    float64\n",
      " 37  month_count_max      169 non-null    int64  \n",
      " 38  month_count_min      169 non-null    int64  \n",
      " 39  avg_month_amt        169 non-null    float64\n",
      " 40  month_amt_std        118 non-null    float64\n",
      " 41  month_amt_max        169 non-null    float64\n",
      " 42  month_amt_min        169 non-null    float64\n",
      " 43  month_count_cv       118 non-null    float64\n",
      " 44  month_amt_cv         118 non-null    float64\n",
      " 45  amt_growth_rate      64 non-null     float64\n",
      " 46  category_count       169 non-null    int64  \n",
      " 47  main_category_ratio  169 non-null    float64\n",
      "dtypes: float64(36), int64(11), object(1)\n",
      "memory usage: 63.5+ KB\n"
     ]
    }
   ],
   "source": [
    "ccd_features_final.info()"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "87b65e3c",
   "metadata": {},
   "source": [
    "### 训练集保存"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 24,
   "id": "ae1967f2",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "\n",
      "特征文件已保存: ./feature/Train\\TRAIN_CCD_TR_DTL_features.pkl\n",
      "文件大小: 0.07 MB\n"
     ]
    }
   ],
   "source": [
    "# 确保特征目录存在\n",
    "feature_dir = './feature/Train'\n",
    "if not os.path.exists(feature_dir):\n",
    "    os.makedirs(feature_dir)\n",
    "    print(f\"创建特征目录: {feature_dir}\")\n",
    "\n",
    "# 保存为pickle格式\n",
    "output_file = os.path.join(feature_dir, 'TRAIN_CCD_TR_DTL_features.pkl')\n",
    "with open(output_file, 'wb') as f:\n",
    "    pickle.dump(ccd_features_final, f)\n",
    "\n",
    "print(f\"\\n特征文件已保存: {output_file}\")\n",
    "print(f\"文件大小: {os.path.getsize(output_file) / 1024 / 1024:.2f} MB\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "07b8d29d",
   "metadata": {},
   "source": [
    "### A测试集"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 42,
   "id": "02fd6516",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "\n",
      "特征文件已保存: ./feature/A\\A_CCD_TR_DTL_features.pkl\n",
      "文件大小: 0.01 MB\n"
     ]
    }
   ],
   "source": [
    "# 确保特征目录存在\n",
    "feature_dir = './feature/A'\n",
    "if not os.path.exists(feature_dir):\n",
    "    os.makedirs(feature_dir)\n",
    "    print(f\"创建特征目录: {feature_dir}\")\n",
    "\n",
    "# 保存为pickle格式\n",
    "output_file = os.path.join(feature_dir, 'A_CCD_TR_DTL_features.pkl')\n",
    "with open(output_file, 'wb') as f:\n",
    "    pickle.dump(ccd_features_final, f)\n",
    "\n",
    "print(f\"\\n特征文件已保存: {output_file}\")\n",
    "print(f\"文件大小: {os.path.getsize(output_file) / 1024 / 1024:.2f} MB\")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "7d78dec2",
   "metadata": {},
   "outputs": [],
   "source": []
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "starcup",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.10.18"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 5
}
